Skip to content

window-rule: add block-pointer-constraints to suppress pointer-lock activation#4045

Closed
joshsymonds wants to merge 5 commits into
niri-wm:mainfrom
joshsymonds:josh/block-pointer-constraints
Closed

window-rule: add block-pointer-constraints to suppress pointer-lock activation#4045
joshsymonds wants to merge 5 commits into
niri-wm:mainfrom
joshsymonds:josh/block-pointer-constraints

Conversation

@joshsymonds
Copy link
Copy Markdown

Summary

Adds a block-pointer-constraints window-rule (Option<bool>) that
suppresses activation of zwp_pointer_constraints_v1 constraints for
matching windows. Both Locked and Confined variants are suppressed
unconditionally — the rule is opt-in per window, so apps that
legitimately need pointer-constraints (games, drawing tablets, 3D
modelers) are unaffected unless explicitly matched.

Motivation

Some clients request a pointer-lock as part of a tooltip / annotation /
overlay UI surface that users don't actually want to engage when the
cursor merely crosses it — the constraint locks the cursor in place
until pointer focus moves away (which from a user's perspective looks
like "the cursor is stuck"). A per-window opt-out preserves the
protocol for its intended uses while letting users disable it for
specific surfaces.

Concrete motivating case: Zoom's annotate_toolbar overlay (a small
floating toolbar that appears during screen-sharing-with-annotation)
requests a pointer-lock on hover. Users only want to interact with the
toolbar deliberately, but on niri the lock activates whenever the
cursor crosses the overlay's region.

Implementation

  • Schema (niri-config/src/window_rule.rs): new
    block_pointer_constraints: Option<bool> field on WindowRule
    adjacent to block_out_from, with the standard
    #[knuffel(child, unwrap(argument))] annotation.
  • Resolution (src/window/mod.rs): same field on
    ResolvedWindowRules, last-Some-wins resolution in compute().
  • Gate (src/niri.rs): Niri::maybe_activate_pointer_constraint()
    short-circuits when the constrained surface's root toplevel matches a
    rule with this flag set. The check is placed inside the
    with_pointer_constraint callback after the constraint-present
    early-return, so the no-constraint fast path that runs on every
    pointer motion is unchanged. Root-surface resolution uses the existing
    Niri::find_root_shell_surface helper (popups + subsurfaces are
    handled correctly).
  • Test: parser test in niri-config/src/lib.rs extends the existing
    window-rule fixture to cover the new property round-tripping.
  • Wiki: new #### block-pointer-constraints section in
    docs/wiki/Configuration:-Window-Rules.md between block-out-from
    and opacity, with the motivating Zoom example.

Design notes

  • Activation-time gate, not bind-time refusal. The protocol still
    binds and the client's request still arrives at smithay; niri just
    never calls constraint.activate(). Well-behaved clients gate
    relative-motion mode and similar features on the constraint being
    active, so they degrade gracefully to normal absolute-motion input.
  • Both variants, unconditionally. A Confined constraint with a
    sufficiently large region produces the same user-visible "cursor
    stuck" pattern as Locked; suppressing both keeps the rule's
    semantics predictable. A future enhancement could split this into an
    enum if a real use case demands variant-specific control.
  • Per-window, opt-in. Global pointer-constraint suppression would
    break games, drawing tablets, and Blender-style middle-drag
    interactions; per-window keeps the rule narrowly scoped.

Test plan

  • cargo build --workspace clean
  • cargo test -p niri-config passes (parser test exercises the new field, wiki-parses test confirms the KDL example parses)
  • cargo clippy --workspace --all-targets -- -D warnings clean
  • cargo fmt --check --all clean
  • Deployed live on Linux/Wayland; config parses, rule resolves on the motivating window
  • End-to-end cursor-suppression on the motivating app (Zoom annotate_toolbar) — verification in progress on next session

joshsymonds and others added 5 commits May 12, 2026 12:38
New per-window-rule bool, default unset. Mirrors the block-out-from
pattern: knuffel #[child, unwrap(argument)] accepting
`block-pointer-constraints true` in KDL. No runtime behavior yet;
that lands in a follow-up commit once ResolvedWindowRules + the
activation gate at maybe_activate_pointer_constraint are in place.

Parser test extends the existing window-rule fixture in lib.rs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds block_pointer_constraints to ResolvedWindowRules with last-Some-
wins resolution in compute(), and short-circuits
Niri::maybe_activate_pointer_constraint when the constrained surface's
root toplevel belongs to a window matched by a rule with this flag
set. The walk-to-root before the layout lookup mirrors the existing
pattern in PointerConstraintsHandler::cursor_position_hint — pointer
constraints can be requested on subsurfaces too, but window rules
resolve on the toplevel.

Both Locked and Confined variants are suppressed unconditionally: the
rule is opt-in per window, so apps that legitimately need either
variant (games, drawing tablets, 3D modelers) are unaffected unless
they match a user-authored rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the wiki section users will hit when they look for "why is my
cursor sticking on an overlay." Example KDL is intentionally generic
(app-id="some-app") rather than naming a specific vendor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four small fixups surfaced by the four-reviewer audit:

- src/niri.rs: replace inline `get_parent`-walk with the existing
  `Niri::find_root_shell_surface` helper. The helper consults the
  cached root_surface map and walks popups to their parent toplevel
  via find_popup_root_surface — the raw walk lacked both. Drops the
  newly-added `get_parent` import, restoring the pre-patch import
  surface in niri.rs.
- src/window/mod.rs: trim the 7-line doc comment on
  ResolvedWindowRules::block_pointer_constraints to a one-line
  summary matching adjacent fields. Long-form prose lives in the
  wiki.
- docs/wiki/Configuration:-Window-Rules.md: change `Since: next` to
  `Since: next release` to match upstream's release-bump workflow
  (commit 8fd9fb7's find/replace targets the longer form). Replace
  the generic app-id="some-app" example with a concrete Zoom
  annotate_toolbar example that matches the originally-stated
  use case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reviewer feedback: the gate ran unconditionally before
with_pointer_constraint, paying for find_root_shell_surface +
find_window_and_output on every pointer motion even when no
constraint exists for the focused surface (which is the common
case — ~100% of motions over non-constraint surfaces).

Relocate the check inside the with_pointer_constraint callback,
after the constraint-present early-return. Restores the pre-patch
fast path: the rule lookup only runs when a constraint actually
exists to be activated.

Semantics are preserved — the block still short-circuits before
constraint.activate(); the only observable difference is where the
short-circuit lives relative to the protocol bind/activate boundary.
The constraint stays bound but inactive either way.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Sempyos Sempyos added area:input Keyboard, mouse, touchpad, tablet, gestures, pointer area:config Config parsing, default config, new settings pr kind:feature New features and functionality labels May 13, 2026
@YaLTeR
Copy link
Copy Markdown
Member

YaLTeR commented May 13, 2026

Thanks, however in this case it sounds like the app is buggy and should fix its behavior.

@joshsymonds
Copy link
Copy Markdown
Author

Maybe, but I think my chances of getting Zoom to do this are pretty low! No worries if you don't want to accept this though.

@joshsymonds
Copy link
Copy Markdown
Author

joshsymonds commented May 14, 2026

Closing.

Today's Discord thread made clear that contributions from anyone using AI assistance aren't welcome here, independent of whether the code is correct. That's a community choice you're entitled to make, but it means there's no point sending further PRs. Keeping this patch on my fork.

@faetalize
Copy link
Copy Markdown

Closing.

Today's Discord thread made clear that contributions from anyone using AI assistance aren't welcome here, independent of whether the code is correct. That's a community choice you're entitled to make, but it means there's no point sending further PRs. Keeping this patch on my fork.

This ain't even true. The author of the project isn't anti-AI in any sense of the word. I'm pretty sure this is a matter of correctness, not just code quality.

@joshsymonds
Copy link
Copy Markdown
Author

@faetalize I was harassed in the community Discord for this PR while @YaLTeR was present and participating, with no pushback from anyone involved. My PR descriptions may have been too long or too AI-flavored, and I will own that -- but I do not think that justifies language like "speedrunning being their most hated person."

I was also told, verbatim: "we dont want purely ai-generated code. end of story."

The rejection wasn't about correctness. It was about the tooling I used to write the PR, and the way I got mocked for using it in this project's Discord. I realize niri has no Code of Conduct, but I'm still disappointed that an open source project of this size allows harassment of contributors in its community spaces.

@faetalize
Copy link
Copy Markdown

@faetalize I was harassed in the community Discord for this PR while @YaLTeR was present and participating, with no pushback from anyone involved. My PR descriptions may have been too long or too AI-flavored, and I will own that -- but I do not think that justifies language like "speedrunning being their most hated person."

I was also told, verbatim: "we dont want purely ai-generated code. end of story."

The rejection wasn't about correctness. It was about the tooling I used to write the PR, and the way I got mocked for using it in this project's Discord. I realize niri has no Code of Conduct, but I'm still disappointed that an open source project of this size allows harassment of contributors in its community spaces.

I saw those comments and they do not represent the opinion of the main developer. The community is mixed and reactionary, and most people in small niches feel very strongly about AI. It is what it is.

Even the main dev himself was once a target of an Anti-AI campaign, where he was demanded to take a stronger stance on AI. I get the impression that the main dev does not want to be involved in these discussions (which makes perfect sense) explaining his abstention from getting involved in the conversation.

People are going to subtweet and screenshot things and whatnot, that's just the name of the game in these communities. But what happens in a Discord channel does not represent at all the Developer's opinions or wishes. When you're building for an Open Source project, minimal interaction with the community is advised especially when AI usage is involved. Highly passionate young adults and teenagers (that think they got the world figured out) will make their opinion known on everything.

The main takeaway from this is that you ought to have discussed these changes (or the motivations behind the PR) before implementing (or having AI implement) it. Making 10/10 code doesn't necessarily imply it would get merged; it needs to align with the dev's vision, and be a feature that makes sense, implemented in a way that's easy to maintain, etc. From what I noticed, the dev seems to not even agree with the principle behind this PR (ie, it's Zoom's fault, not niri's) and these details matter and need to be established with the lead dev before writing a single line of code.

@faetalize
Copy link
Copy Markdown

Actually, I think I'm overstepping with the response above. I shouldn't speak for the developer or presume to know what their opinion/stance is. Also, I apologize if my comment might come off as dismissive of your experience. I still maintain that discussion about the patch should have happened before implementation.

@joshsymonds
Copy link
Copy Markdown
Author

@faetalize I appreciate your apology! To be clear about what I was reacting to: I was opening what I thought were useful PRs, and was shocked to discover I was being mocked in the project Discord with the maintainer present and participating. That's an unusual experience in my time contributing to OSS, to put it lightly.

To the substance.

The "you should've discussed this" framing is orthogonal to what happened. I was never told that by anyone here or in the Discord. @YaLTeR's stated reason for declining was "the app is buggy and should fix its behavior." And yes, agreed, Zoom should fix this. Until that time, which I'm sure is imminent, I've fixed it upstream.

The Discord pile-on was about AI provenance. The process angle was raised only by you, and only after I was already harassed away from this community. Pre-discussing the change wouldn't have shifted YaLTeR's view that this is Zoom's problem, and it wouldn't have stopped anyone from calling working code "slop." The advice has nothing to do with what actually happened here.

Even setting that aside, this PR is utterly trivial: a single opt-in boolean on WindowRule with last-Some-wins resolution and a gate inside an existing callback. It changes zero behavior for anyone who doesn't write the rule. I had more substantive changes merged into Rails core years ago with no prior discussion. "Discuss before substantial PRs" doesn't apply to small opt-in additive features.

On the harassment piece: "that's just the name of the game" only works as a defense if the people with authority push back. The actual owner of the community was right there, participating in the discussion himself. When that happens, silence functions as endorsement -- it says explicitly that this is allowed here and accepted as a community norm, and that is how I experienced this. Framing the harassers as "passionate young adults and teenagers (that think they got the world figured out)" doesn't change the fact that it happened and was a-okay with everyone involved.

Harassing people is bad and should be against your rules. Unless you or the niri project are seriously arguing that people treating each other like dirt is an acceptable part of how this community operates?

@Aaxper
Copy link
Copy Markdown

Aaxper commented May 14, 2026

As the person responsible for the "we dont want purely ai-generated code. end of story." comment, I would encourage anyone concerned about Josh's claims of "harassment" to look at the actual conversation that took place. For those who aren't on the Niri Discord server, I have attached screenshots.
image
image

@joshsymonds
Copy link
Copy Markdown
Author

Thanks, I like PRs that are self-documenting! Could you also attach the screenshots of someone saying I was "speedrunning to be their most-hated person" while the maintainer responded 4 minutes later -- but not to that comment? I think it would be useful for other contributors to see as well.

@Aaxper
Copy link
Copy Markdown

Aaxper commented May 14, 2026

image

Yes, here is the screenshot: Someone saying that to someone else entirely, where both users were clearly joking.

@joshsymonds
Copy link
Copy Markdown
Author

Oh peep -- okay, sorry about that one! In the heat of the moment when I saw people were mocking me, I read that and assumed it was about me too: it was not and I'm sorry to have dragged it in.

That said, my larger point about being harassed for my contributions stands: my PR was posted on the niri community Discord because people didn't like it and was then subsequently crapped on, including by the maintainer himself. Like, I was contributing in good faith and I guess I had an expectation that I would be engaged with in good faith in return?

As I said earlier, I've been involved in other OSS projects and this is the first time I've seen behavior like this. If you didn't like the PR or wanted the description different that's fine -- like, talk to me about it? Or post on the PR?

Or subtweet me on your Discord I guess, it's your community. Just not what I'd consider contributor-friendly behavior, but I guess if contributors aren't using community-approved tooling contributions aren't welcome anyway.

Separately, while I do not think you specifically were harassing me, you definitely did say that the project doesn't want AI code, which contradicts what @faetalize said, sooo I dunno, guess y'all should square that too.

@Aaxper
Copy link
Copy Markdown

Aaxper commented May 15, 2026

It can be true that you should have talked it through with the maintainers and that we don't tend to like accepting slop into the Niri codebase. For future reference, we are fine with AI assistance, but do not have AI entirely author a PR on your behalf and expect us to merge it.

If the messages that you specifically cited as harassment were not harassment, then I'd be very interested to see which messages you thought were.

I'm sorry if you felt that way about our comments, but we receive quite a few slop PRs and none of us have the care to go through and individually deal with any one of them to much extent. Given that and how little I care about vibecoding, I will no longer be replying here either.

@joshsymonds
Copy link
Copy Markdown
Author

Who was expecting you to merge anything? I said in my first reply that if you didn't want to merge it, that's fine -- I can maintain a fork. I was just trying to add a feature I found useful, thinking other people who use Zoom on niri would benefit too: nothing more.

My expectation was simply that I wouldn't be treated like crap in your community: a bar that you and the other community members here failed to meet and continue to fail to meet.

When I authored my PR I did not expect phisch's "I didn't even find the patience to read the text of that PR" and "just blindly letting the AI do everything is lazy and puts the work on the maintainer," and YaLTeR's "even the ai slop aside." Certainly that kind of conduct about a PR is unbecoming -- which I guess is why it was said behind my back in your Discord, instead of on my actual PR.

And I think it's incredibly disingenuous of you to imply this is somehow my fault for not having come to the maintainers first or because of the quality of the code. Is that a prerequisite for PRs to be afforded the respect of not being posted to your Discord and mocked there? I assume that's somewhere in your CONTRIBUTING.md and I just missed it?

If the response is "what you experienced reflects our policy of how we engage with AI-coauthored PRs" -- having such a policy is your prerogative. Communicating it on the PR, via "thanks, we don't accept AI contributions, closing" would have been how you should actually enforce it. And I would suggest you act more professionally in the future: perhaps you should add something about this to your CONTRIBUTING.md, so other people who want to improve the quality of your project know to spend their time elsewhere.

joshsymonds added a commit to joshsymonds/niri that referenced this pull request May 15, 2026
…anup

Three confirmed review improvements:

- Doc-comments on `WindowRule::block_focus_cursor_warp` and
  `ResolvedWindowRules::block_focus_cursor_warp` were inaccurate: they
  named `Niri::maybe_warp_cursor_to_focus` as the function that
  short-circuits, but the gate actually lives in
  `Niri::move_cursor_to_focused_tile`. Updated both, and noted that the
  single chokepoint covers both `maybe_warp_cursor_to_focus` and
  `maybe_warp_cursor_to_focus_centered` (and any future warp wrapper).

- Added a fourth gate test, `gate_does_not_fire_when_rule_does_not_match`.
  Covers the rule-resolution loop's "skip non-matching rule" branch
  for this field — a regression that moved the merge outside the
  matcher guard would leak `Some(true)` across to unrelated windows.
  The pre-existing "no rule at all" test couldn't catch this since
  its config had no rules, leaving the loop body unreachable.

- Simplified the `map_titled_window` test helper. All three (now four)
  call sites destructured into `_id, _surface` and never used them;
  helper now returns `Fixture` directly. Drops the unused
  `client::ClientId` and `wayland_client::WlSurface` imports.

Skipped (documented): wiki docs entry in
`docs/wiki/Configuration:-Window-Rules.md`. Same rationale as the
previous epic's S-I1 finding: fork-only patch (upstream PR niri-wm#4045 for
the sibling `block-pointer-constraints` was closed unmerged, see
INTEGRATION.md), the wiki file is upstream's, adding to it creates
rebase friction with no consumer.
@YaLTeR
Copy link
Copy Markdown
Member

YaLTeR commented May 15, 2026

in case you missed my responses to your discord messages (after presumably leaving):

image

also,

crapped on, including by the maintainer himself

..no? excuse me?

and YaLTeR's "even the ai slop aside."

well, yes, if I see an ai-written description and 5 commits co-authored by claude, I will call it "ai slop". as I mentioned I haven't looked at the patches so I can't say whether it is actually slop, but I am still gonna half-mockingly call something like this "ai slop". I will do it even in positive contexts. It does not represent an automatic overwhelmingly-negative opinion of the PR, the code, or the author. I was under the impression it's also a rather commonplace expression nowadays?

@joshsymonds
Copy link
Copy Markdown
Author

I appreciate the points you made in Discord after I left. The maintenance point is fair -- you carry these patches forever, not me. And I get why a stream of low-effort AI PRs is exhausting; I deal with it at work constantly.

But the claim that "ai slop" is a neutral term you'd use even in praise is unserious. The conversation about my PR was clearly negative, and you participated in it.

To be clear, I didn't pursue any of this. I closed the PR with a measured note saying exactly why: the Discord had made clear AI-assisted contributions weren't wanted, and aaxper confirmed as much themselves. I was met with hostility, left immediately, and closed all my PRs. Then @faetalize quoted me back in; I engaged in good faith; only then did the screenshots and "presumably leaving" show up.

The message even shifts depending on the room: publicly, "we're fine with AI assistance"; in the Discord, "end of story." I read that as an invitation to leave, and gladly took it.

On "presumably leaving:" by your own account you took me to have gone, then composed replies to someone you'd assumed had left. How did you intend for me to read your response?

What's truly galling is that my commit is 54 lines in total, and around 40 of those are documentation and comments. The functional part is roughly ten lines in src/niri.rs. Classifying it as slop in the Discord, mocking it roundly, rationalizing it afterward, screenshotting that here as evidence, and arguing it across fifteen comments cost far more than the review ever would have. The code was never evaluated and the feature never engaged on its merits -- it was waved off as Zoom's problem, then mocked in private. Surely it would have been easier to just read it, or to post a one-line "please make the description less AI-y?"

Where I come from, people who act badly own it and try to do better. They don't tell the person they treated badly that he deserved it and it wasn't that bad anyway. That's conduct that hurts people who show up in good faith. It hurt me, and I left because of how you and this community behaved.

My fork works and fixes the problem; whether it's upstreamed has been irrelevant to me since my first reply, where I said exactly that. I won't be sending more. The only people worse off for any of this are others running Zoom on niri.

I hope this thread is instructive for others looking to contribute here. I know I will be using it as an exemplar of how this project and its maintainers behave in public and private.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:config Config parsing, default config, new settings area:input Keyboard, mouse, touchpad, tablet, gestures, pointer pr kind:feature New features and functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants